# Java 异常机制
异常是程序中的一些错误,有时候错误是无法避免的,有些错误是用户引起的,有些错误是程序引起的,所以需要一套异常处理机制来处理这些错误。
# 异常的层次结构
# Throwable
Throwable 是 Java 中所有错误、异常的顶级父类,向下可以分为 Error(错误) 和 Exception(异常)两种类型。
Throwable 提供了 printStackTrace() 等方法,可以在发生异常时打印异常的调用栈信息,方便排查问题。
# Error
Error 是虚拟机在运行时产生了致命的严重问题,应用程序不应该去处理这类问题。
比如 OutOfMemoryError:内存不足,StackOverflowError:栈溢出。
# Exception
Exception 是可以捕获并由应用程序处理的异常,可以分为两类:运行时异常和异常
运行时异常是 RuntimeException 类及其子类,运行时异常不需要在编译时显式地进行捕获或者声明,而是在运行时可能抛出并有程序的调用者进行处理,也叫不可查异常。
编译时异常是 Exception 的子类,但并不是 RuntimeException 子类,编译时异常时需要在编译时强制要求程序进行捕获或者声明,以确保异常能被正常处理或者传递,也叫可查异常。
# 异常的使用
# 异常关键字
try :用于定义一个包含可能抛出异常的代码块。
catch :用于捕获和处理 try 块中抛出的异常。catch 在后面跟上要捕获的异常类型,然后对相应的异常进行处理。
finally :用于定义一个无法是否发生异常都会执行的代码块。finally 代码块通常用来做关闭连接、释放资源等必要操作。
throw :用于代码中抛出一个异常,后接一个异常对象。
throws :用于方法声明中指定该方法可能抛出的异常,后接一个或者多个异常类型。如果抛出的是编译时异常,则调用方必须要对可能抛出的异常进行处理。
try-with-resources : 是 Java7 引入的一个语法糖,用于简化资源的获取与释放代码。对于实现了 AutoCloseable 接口的资源,无需现实地在 finally 块中进行释放
# 异常关键字使用示例
配合注释去运行体会
import java.util.concurrent.TimeUnit;
/**
* @author linjinjia
* @date 2023/7/3 17:57
*/
public class ExceptionUsage {
/**
* 自定义检查异常(运行时异常)
*/
private static class MyCheckedException extends Exception {
}
/**
* 自定义不可查异常(编译时异常)
*/
private static class MyUncheckedException extends RuntimeException {
}
/**
* 自定义自动关闭的资源
*/
private static class AutoCloseableResource implements AutoCloseable {
@Override
public void close() throws Exception {
// 使用 try-with-resources 的方式,
// 即使我们没有显式调用这个方法,它也会自动执行
System.out.println("try-with-resources: 自动关闭资源");
}
}
/**
* 该方法会声明并抛出一个编译时异常,调用方必须处理异常
*/
public static void throwCheckedException() throws MyCheckedException {
System.out.println("throwCheckedException: 抛出编译时异常");
throw new MyCheckedException();
}
/**
* 该方法会声明并抛出一个运行时异常,调用方不是必须处理异常
*/
public static void throwUncheckedException() throws MyUncheckedException {
System.out.println("throwUncheckedException: 抛出运行时异常");
throw new MyUncheckedException();
}
/**
* 演示关键字的使用
*/
public static void tryCatchFinally() {
try {
throwCheckedException();
} catch (MyCheckedException e) {
// 编译时异常必须被处理
// 可以将这个 catch 块删除,然后编译,会不通过
System.out.println("tryCatchFinally: 处理编译时异常");
e.printStackTrace();
} finally {
System.out.println("tryCatchFinally: finally 块一定会被执行");
}
try {
// 不用去处理这个方法抛出的异常,照样可以编译成功,
// 只不过在运行时不处理会导致线程结束
throwUncheckedException();
} catch (MyUncheckedException e) {
// 可以将这个 catch 块删除,然后编译,可以通过
// 再运行则会报错
System.out.println("tryCatchFinally: 处理运行时异常");
e.printStackTrace();
} finally {
System.out.println("tryCatchFinally: finally 块一定会被执行");
}
}
/**
* 演示 try-with-resources 的使用
*/
public static void tryWithResources() {
try (AutoCloseableResource r = new AutoCloseableResource()) {
// doSomething
} catch (Exception e) {
System.out.println("tryWithResources: 处理异常");
}
}
/**
* try-finally 也搭配使用,不一定需要 catch 异常
* 通常用在需要保证某部分代码一定需要被执行的情况,比如锁的释放
*/
public static void tryFinally() {
try {
System.out.println("tryFinally: try 代码块");
} finally {
System.out.println("tryFinally: finally 代码块");
}
}
public static void main(String[] args) throws Exception {
tryCatchFinally();
TimeUnit.SECONDS.sleep(1);
System.out.println("\n分割线 -------------------- \n");
tryWithResources();
System.out.println("\n分割线 -------------------- \n");
tryFinally();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
# 异常的最佳实践
# 使用 finally 或者 try-with-resources 来清理资源
- finally 可以保证资源释放的代码一定会被执行;
- try-with-resources 对于实现了 AutoCloseable 接口的资源,可以自动关闭,简化代码。
# 异常不要用来做流程控制,条件控制
异常设计的初衷是解决程序运行中的各种意外情况,且异常的处理效率比条件判断方式要低很多。
很多问题可以通过提前判断,来规避异常。比如 NullPointerException,IndexOutOfBoundsException 等等。
而对于解析字符串转数字的情况,可能存在数字错误,可以通过 catch NumberFormatException 实现。
# 对异常进行文档说明
定义异常的时候,需要添加注释明确异常已经在什么情况下使用,避免被滥用。
在方法声明出如果有抛出异常,需要添加 Javadoc 的 @throws 进行描述异常在什么情况下抛出,以方便调用者处理异常。
/**
* xxx
* @throws BusinessException 业务异常,xxx
*/
public void test() throws BusinessException {
// doSomething
}
2
3
4
5
6
7
# 尽量复用已有的异常
复用大家熟悉的异常,可以减少别人的代码理解时间,也可以使得你的 API 易于理解和使用。
但是异常的复用是建立在语义之上的,要结合异常的文档来使用,避免出现与异常原本用途不一致的情况。
# 优先捕获明确的异常
当有多个 catch 块的时候,应当将更明确的异常放在前面。
这里的更明确指的是子类,因为异常的层次结构来看,越底层语义应当越明确。
比如 SQLSyntaxErrorException 是 SQLException 子类,拥有更明确的语义,因此捕获的时候,应当优先考虑捕获 SQLSyntaxErrorException。
public void execSql() {
try {
// doSomething
} catch (SQLSyntaxErrorException e) {
log.error(e);
} catch (SQLException e) {
log.error(e)
}
}
2
3
4
5
6
7
8
9
之所以要把明确的异常放在前面,是因为如果把 SQLException 放在前面,那么异常就被第一个 catch 块捕获,后面的 catch 块即使异常类型符合也不会执行了。
# 不要捕获 Throwable 类
正如前面提到的, Throwable 包含了 Error 和 Exception,而 Error 是不应该在应用程序中去处理的严重问题。所以一般不要去捕获 Throwable 类。
# 不要忽略捕获的异常
捕获异常是为了处理它,不要捕获了却什么都不处理而抛弃之,如果不想处理它, 请将该异常抛给它的调用者。最外层的业务使用者,必须处理异常,将其转化为用户可以理解的内容。
如果不需要调用者处理,可以捕获之后通过日志打印错误信息,方便后期排查问题。
# 包装异常时不要抛弃原始的异常
捕获标准异常并包装为自定义异常时,需要将原本的标准异常传入构造方法,作为自定义异常的 cause 。这样做是为了后面可以正常打印自定义异常的堆栈信息,方便问题的排查。
# 不要在 finally 块中使用 return
这样执行 finally 块的时候,会直接通过 return 返回,而不会调用 try 块中的 return。
# 异常的底层原理
# 从字节码看异常
Java 异常的底层原理涉及到 JVM 的异常处理机制,每当发生异常时,JVM 会按照一定的规则来处理异常。而这一套处理流程是建立在异常表 Exception table 的基础上进行的。
下面用一个简单的例子,从字节码的层面来看 JVM 是怎么处理异常的。
public class ExceptionTest {
public static void throwExcep() {
int i = 1 / 0;
}
public static void main(String[] args) {
try {
throwExcep();
} catch (ArithmeticException e1) {
e1.printStackTrace();
} catch (Exception e2) {
e2.printStackTrace();
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
接下来使用命令 javac ExceptionTest.java
将代码编译成字节码,然后使用 javap -c ExceptionTest
打印字节码信息。
javap 是 JDK 自带的命令行工具,用于反汇编字节码文件,-c 选项表示打印字节码指令
Compiled from "ExceptionTest.java"
public class ExceptionTest {
public ExceptionTest();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void throwExcep();
Code:
0: iconst_1
1: iconst_0
2: idiv
3: istore_0
4: return
public static void main(java.lang.String[]);
Code:
0: invokestatic #2 // Method throwExcep:()V
3: goto 19
6: astore_1
7: aload_1
8: invokevirtual #4 // Method java/lang/ArithmeticException.printStackTrace:()V
11: goto 19
14: astore_1
15: aload_1
16: invokevirtual #6 // Method java/lang/Exception.printStackTrace:()V
19: return
Exception table:
from to target type
0 3 6 Class java/lang/ArithmeticException
0 3 14 Class java/lang/Exception
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
文件的尾部出现了前面提的 Exception table,它包含了四个信息
- from:可能发生异常的起点。
- to:可能发生异常的重点。
- target:在 from 和 to 之间发生异常后的异常处理位置。
- type:异常处理位置能处理的异常类型,如果为 any 则表示这部分代码一定会被执行。
所以上述的异常表的第一行表示,在 和 之间发生了 ArithmeticException 以后的话,会由位置 的指令进行处理。
上面是 try-catch 的处理方式,如果 try-catch-finally 的话,则会复杂一些。从 javap 的结果来看,字节码指令会保证 finally 代码块一定会被执行。
public class exceptiontest2 {
public static void throwexcep() {
int i = 1 / 0;
}
public static string wrap() {
try {
throwexcep();
return "ok";
} catch (exception e2) {
e2.printstacktrace();
return "error";
} finally {
system.out.println("finally block");
}
}
public static void main(string[] args) {
wrap();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
javap -c ExceptionTest2 的结果
Compiled from "ExceptionTest2.java"
public class ExceptionTest2 {
public ExceptionTest2();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void throwExcep();
Code:
0: iconst_1
1: iconst_0
2: idiv
3: istore_0
4: return
public static java.lang.String wrap();
Code:
0: invokestatic #2 // Method throwExcep:()V
3: ldc #3 // String OK
5: astore_0
6: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
9: ldc #5 // String finally block
11: invokevirtual #6 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
14: aload_0
15: areturn
16: astore_0
17: aload_0
18: invokevirtual #8 // Method java/lang/Exception.printStackTrace:()V
21: ldc #9 // String Error
23: astore_1
24: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
27: ldc #5 // String finally block
29: invokevirtual #6 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
32: aload_1
33: areturn
34: astore_2
35: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
38: ldc #5 // String finally block
40: invokevirtual #6 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
43: aload_2
44: athrow
Exception table:
from to target type
0 6 16 Class java/lang/Exception
0 6 34 any
16 24 34 any
public static void main(java.lang.String[]);
Code:
0: invokestatic #10 // Method wrap:()Ljava/lang/String;
3: pop
4: return
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
从 wrap 方法的字节码指令来看,finally 块的指令被复制到多个地方以保证一定会被执行。
如果觉得上述字节码指令太复杂,可以使用下面的代码进行测试。
public class ExceptionTest3 {
public static void main(String[] args) {
try {
int a = 1;
} finally {
System.out.println("finnaly block");
}
}
}
2
3
4
5
6
7
8
9
10
# 异常表的使用流程
如果当前线程是最后一个非守护线程,那么就不是线程终止,而是 JVM 直接停止运行。
# 代码下载
文中出现的代码:点击下载 (opens new window)